# OneToManyMetadataResolver.java

    package org.codefilarete.stalactite.engine.configurer.dslresolver;

    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    import java.util.function.Supplier;
    import java.util.stream.Collectors;

    import org.codefilarete.reflection.Accessor;
    import org.codefilarete.reflection.AccessorDefinition;
    import org.codefilarete.reflection.Mutator;
    import org.codefilarete.reflection.PropertyMutator;
    import org.codefilarete.reflection.ReversibleMutator;
    import org.codefilarete.stalactite.dsl.MappingConfigurationException;
    import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration;
    import org.codefilarete.stalactite.dsl.naming.AssociationTableNamingStrategy;
    import org.codefilarete.stalactite.dsl.naming.AssociationTableNamingStrategy.ReferencedColumnNames;
    import org.codefilarete.stalactite.dsl.naming.ColumnNamingStrategy;
    import org.codefilarete.stalactite.dsl.naming.ForeignKeyNamingStrategy;
    import org.codefilarete.stalactite.dsl.naming.JoinColumnNamingStrategy;
    import org.codefilarete.stalactite.dsl.property.CascadeOptions;
    import org.codefilarete.stalactite.engine.configurer.NamingConfiguration;
    import org.codefilarete.stalactite.engine.configurer.dslresolver.InheritanceConfigurationResolver.ResolvedConfiguration;
    import org.codefilarete.stalactite.engine.configurer.dslresolver.MetadataSolvingCache.EntitySource;
    import org.codefilarete.stalactite.engine.configurer.model.DirectRelationJoin;
    import org.codefilarete.stalactite.engine.configurer.model.Entity;
    import org.codefilarete.stalactite.engine.configurer.model.IntermediaryRelationJoin;
    import org.codefilarete.stalactite.engine.configurer.model.RelationJoin;
    import org.codefilarete.stalactite.engine.configurer.model.ResolvedOneToManyRelation;
    import org.codefilarete.stalactite.engine.configurer.onetomany.OneToManyRelation;
    import org.codefilarete.stalactite.engine.runtime.AssociationTable;
    import org.codefilarete.stalactite.engine.runtime.IndexedAssociationTable;
    import org.codefilarete.stalactite.sql.ConnectionConfiguration;
    import org.codefilarete.stalactite.sql.Dialect;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.stalactite.sql.ddl.structure.ForeignKey;
    import org.codefilarete.stalactite.sql.ddl.structure.Key;
    import org.codefilarete.stalactite.sql.ddl.structure.PrimaryKey;
    import org.codefilarete.stalactite.sql.ddl.structure.Table;
    import org.codefilarete.stalactite.sql.result.BeanRelationFixer;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.bean.Objects;
    import org.codefilarete.tool.collection.Iterables;
    import org.codefilarete.tool.collection.KeepOrderSet;

    import static org.codefilarete.tool.Nullable.nullable;
    import static org.codefilarete.tool.collection.Iterables.first;

    public class OneToManyMetadataResolver {

    	private final Dialect dialect;
    	private final ConnectionConfiguration connectionConfiguration;

    	public OneToManyMetadataResolver(Dialect dialect, ConnectionConfiguration connectionConfiguration) {
    		this.dialect = dialect;
    		this.connectionConfiguration = connectionConfiguration;
    	}

    	<C, I> Set<EntitySource<?, ?>> resolve(EntitySource<C, I> source) {
    		KeepOrderSet<EntitySource<?, ?>> targetEntities = new KeepOrderSet<>();
    		// configuring one-to-manys owned by this entity
    		source.getResolvedConfigurations().forEach(resolvedConfiguration -> {
    			targetEntities.addAll(resolve(source.getEntity(), resolvedConfiguration.getMappingConfiguration()));
    		});
    		return targetEntities;
    	}

    	private <C, I> Set<EntitySource<?, ?>> resolve(Entity<C, I, ?> entity, EntityMappingConfiguration<C, I> mappingConfiguration) {
    		KeepOrderSet<EntitySource<?, ?>> targetEntities = new KeepOrderSet<>();
    		mappingConfiguration.getOneToManys().forEach(oneToMany -> {
    			EntitySource<Object, Object> resolve = this.resolve(entity, oneToMany);
    			targetEntities.add(resolve);
    		});
    		// treating relations embedded in insets
    		mappingConfiguration.getPropertiesMapping().getInsets().forEach(inset -> {
    			inset.getConfigurationProvider().getConfiguration().getOneToManys().forEach(oneToMany -> {
    				EntitySource<Object, Object> resolve = this.resolve(entity, oneToMany.embedInto(inset.getAccessor(), inset.getEmbeddedClass()));
    				targetEntities.add(resolve);
    			});
    		});
    		return targetEntities;
    	}

    	<SRC, TRGT, S extends Collection<TRGT>, SRCID, TRGTID, SRCTABLE extends Table<SRCTABLE>, TRGTTABLE extends Table<TRGTTABLE>>
    	EntitySource<TRGT, TRGTID> resolve(Entity<SRC, SRCID, SRCTABLE> source, OneToManyRelation<SRC, TRGT, TRGTID, S> oneToMany) {

    		EntitySource<TRGT, TRGTID> targetEntitySource = buildTargetEntity(oneToMany);
    		NamingConfiguration namingConfiguration = first(targetEntitySource.getResolvedConfigurations()).getNamingConfiguration();

    		PropertyMutator<TRGT, SRC> reverseAccessPoint = oneToMany.giveReverseSetter();
    		RelationJoin tablesJoin = null;
    		BeanRelationFixer<SRC, TRGT> relationFixer;
    		Entity<TRGT, TRGTID, TRGTTABLE> targetEntity = targetEntitySource.getEntity();
    		AccessorDefinition collectionAccessorDefinition = AccessorDefinition.giveDefinition(oneToMany.getCollectionAccessor());

    		Supplier<S> collectionFactory = oneToMany.getCollectionFactory();
    		if (collectionFactory == null) {
    			collectionFactory = Reflections.giveCollectionFactory((Class<S>) collectionAccessorDefinition.getMemberType());
    		}

    		AccessorDefinition accessorDefinitionForTableNaming = new AccessorDefinition(
    				collectionAccessorDefinition.getDeclaringClass(),
    				collectionAccessorDefinition.getName(),
    				// we prefer the target persister type to method reference member type because the latter only gets the collection type which is not
    				// valuable information for table / column naming
    				oneToMany.getTargetMappingConfiguration().getEntityType());

    		Column<TRGTTABLE, Integer> indexingColumn = null;
    		if (oneToMany.isOwnedByReverseSide()) {
    			// target owns the relation
    			// we don't create foreign key for table-per-class because source columns should reference different tables (the on per entity) which databases do not allow
    			boolean canCreateForeignKey = !source.isTablePerClass();
    			if (canCreateForeignKey) {
    				if (!targetEntity.isTablePerClass()) {
    					OneToManyOwnedByTargetHelper<SRC, TRGT, SRCID, TRGTID, SRCTABLE, TRGTTABLE> helper = new OneToManyOwnedByTargetHelper<>();
    					tablesJoin = helper.determineJoin(oneToMany, source.getTable().getPrimaryKey(), targetEntity.getTable(), namingConfiguration.getJoinColumnNamingStrategy(), namingConfiguration.getForeignKeyNamingStrategy());
    				}
    			} // else: creating foreign key is not possible, nothing special to do

    			Mutator<TRGT, SRC> NOOP_REVERSE_SETTER = (o, i) -> {};
    			relationFixer = BeanRelationFixer.of(oneToMany.getCollectionAccessor(), collectionFactory, Objects.preventNull(oneToMany.getReverseLink(), NOOP_REVERSE_SETTER));

    			if (oneToMany.isOrdered()) {
    				indexingColumn = oneToMany.getIndexingColumn();
    				if (indexingColumn == null) {

    					String indexingColumnName = nullable(oneToMany.getIndexingColumnName()).getOr(() -> namingConfiguration.getIndexColumnNamingStrategy().giveName(accessorDefinitionForTableNaming));
    					Class indexColumnType = oneToMany.getRelationMode() == CascadeOptions.RelationMode.ALL_ORPHAN_REMOVAL
    							? int.class
    							: Integer.class;	// column must be nullable since row won't be deleted through orphan removal but only "detached" from parent row
    					indexingColumn = targetEntity.getTable().addColumn(indexingColumnName, indexColumnType);
    				}
    			}

    		} else {
    			// an association table is necessary to link source and target entities
    			// we don't create foreign key for table-per-class because source columns should reference different tables (the on per entity) which databases do not allow
    			boolean canCreateForeignKey = !source.isTablePerClass();
    			if (canCreateForeignKey) {
    				if (!targetEntity.isTablePerClass()) {
    					OneToManyWithAssociationTableHelper<SRC, TRGT, SRCID, TRGTID, SRCTABLE, TRGTTABLE, ?> helper = new OneToManyWithAssociationTableHelper<>(accessorDefinitionForTableNaming);
    					tablesJoin = helper.determineJoin(
    							oneToMany,
    							source.getTable().getPrimaryKey(),
    							targetEntity.getTable(),
    							namingConfiguration.getAssociationTableNamingStrategy(),
    							namingConfiguration.getForeignKeyNamingStrategy(),
    							namingConfiguration.getIndexColumnNamingStrategy());
    				}
    			} // else: creating foreign key is not possible, nothing special to do

    			Mutator<TRGT, SRC> NOOP_REVERSE_SETTER = (o, i) -> {};
    			relationFixer = BeanRelationFixer.of(oneToMany.getCollectionAccessor(), collectionFactory, Objects.preventNull(oneToMany.getReverseLink(), NOOP_REVERSE_SETTER));

    			if (oneToMany.isOrdered()) {
    				indexingColumn = oneToMany.getIndexingColumn();
    				if (indexingColumn == null) {

    					String indexingColumnName = nullable(oneToMany.getIndexingColumnName()).getOr(() -> namingConfiguration.getIndexColumnNamingStrategy().giveName(accessorDefinitionForTableNaming));
    					Class indexColumnType = oneToMany.getRelationMode() == CascadeOptions.RelationMode.ALL_ORPHAN_REMOVAL
    							? int.class
    							: Integer.class;	// column must be nullable since row won't be deleted through orphan removal but only "detached" from parent row
    					indexingColumn = targetEntity.getTable().addColumn(indexingColumnName, indexColumnType);
    				}
    			}
    		}

    		ResolvedOneToManyRelation<SRC, TRGT, S, SRCID, TRGTID, SRCTABLE, TRGTTABLE> entitiesLink = new ResolvedOneToManyRelation<>(
    				targetEntity,
    				oneToMany.getCollectionAccessor(),
    				oneToMany.getRelationMode(),
    				oneToMany.isOwnedByReverseSide(),
    				reverseAccessPoint,
    				oneToMany.isFetchSeparately(),
    				tablesJoin,
    				relationFixer,
    				collectionFactory,
    				indexingColumn
    		);
    		source.addRelation(entitiesLink);

    		return targetEntitySource;
    	}

    	private <SRC, TRGT, S extends Collection<TRGT>, TRGTID> EntitySource<TRGT, TRGTID>
    	buildTargetEntity(OneToManyRelation<SRC, TRGT, TRGTID, S> oneToMany) {
    		InheritanceConfigurationResolver<TRGT, TRGTID> inheritanceConfigurationResolver = new InheritanceConfigurationResolver<>();
    		KeepOrderSet<ResolvedConfiguration<?, TRGTID>> ancestorsConfigurations = inheritanceConfigurationResolver.resolveConfigurations(oneToMany.getTargetMappingConfiguration());

    		InheritanceMetadataResolver<TRGT, TRGTID, ?> keyMappingApplier = new InheritanceMetadataResolver<>(dialect, connectionConfiguration);
    		return keyMappingApplier.resolve(ancestorsConfigurations);
    	}

    	private static class OneToManyOwnedByTargetHelper<SRC, TRGT, SRCID, TRGTID, LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>> {

    		protected RelationJoin determineJoin(OneToManyRelation<SRC, TRGT, TRGTID, ?> relation,
    		                                     PrimaryKey<LEFTTABLE, SRCID> leftPrimaryKey,
    		                                     RIGHTTABLE targetTable,
    		                                     JoinColumnNamingStrategy joinColumnNamingStrategy,
    		                                     ForeignKeyNamingStrategy foreignKeyNamingStrategy) {
    			ForeignKey<RIGHTTABLE, LEFTTABLE, SRCID> foreignKey = determineForeignKeyColumns(relation, leftPrimaryKey, targetTable, joinColumnNamingStrategy, foreignKeyNamingStrategy);

    			return new DirectRelationJoin<>(foreignKey.getReferencedKey(), foreignKey);
    		}

    		private ForeignKey<RIGHTTABLE, LEFTTABLE, SRCID> determineForeignKeyColumns(OneToManyRelation<SRC, TRGT, TRGTID, ?> relation,
    		                                                                              PrimaryKey<LEFTTABLE, SRCID> leftPrimaryKey,
    		                                                                              RIGHTTABLE targetTable,
    		                                                                              JoinColumnNamingStrategy joinColumnNamingStrategy,
    		                                                                              ForeignKeyNamingStrategy foreignKeyNamingStrategy) {
    			Key.KeyBuilder<RIGHTTABLE, SRCID> foreignKeyBuilder = Key.from(targetTable);
    			if (!relation.getForeignKeyNameMapping().isEmpty()) {
    				Map<Accessor<SRCID, ?>, Column<RIGHTTABLE, ?>> foreignKeyColumnMapping = new HashMap<>();
    				relation.getForeignKeyNameMapping().forEach((valueAccessPoint, colName) -> {
    					AccessorDefinition localAccessorDefinition = AccessorDefinition.giveDefinition(valueAccessPoint);
    					Accessor<SRCID, Object> accessor = valueAccessPoint instanceof Accessor
    							? (Accessor) valueAccessPoint
    							: (valueAccessPoint instanceof ReversibleMutator ? ((ReversibleMutator) valueAccessPoint).toAccessor() : null);
    					if (accessor == null) {
    						throw new UnsupportedOperationException("Can't get accessor from " + valueAccessPoint);
    					}
    					Column<RIGHTTABLE, ?> column = targetTable.addColumn(colName, localAccessorDefinition.getMemberType());
    					foreignKeyBuilder.addColumn(column);
    					foreignKeyColumnMapping.put(accessor, column);
    				});
    			} else if (!relation.getForeignKeyColumnMapping().isEmpty()) {
    				Map<Accessor<SRCID, ?>, Column<RIGHTTABLE, ?>> foreignKeyColumnMapping = new HashMap<>();
    				relation.getForeignKeyColumnMapping().forEach((valueAccessPoint, column) -> {
    					Accessor<SRCID, Object> accessor = valueAccessPoint instanceof Accessor
    							? (Accessor) valueAccessPoint
    							: (valueAccessPoint instanceof ReversibleMutator ? ((ReversibleMutator) valueAccessPoint).toAccessor() : null);
    					if (accessor == null) {
    						throw new UnsupportedOperationException("Can't get accessor from " + valueAccessPoint);
    					}
    					foreignKeyBuilder.addColumn((Column<RIGHTTABLE, ?>) column);
    					foreignKeyColumnMapping.put(accessor, (Column<RIGHTTABLE, ?>) column);
    				});
    			} else if (relation.getReverseColumnName() != null || relation.getReverseColumn() != null) {
    				Column<RIGHTTABLE, ?> reverseColumn;
    				if (relation.getReverseColumnName() != null) {
    					PrimaryKey<LEFTTABLE, SRCID> srcPrimaryKey = leftPrimaryKey;
    					// with a reverse column name, the primary key should be a single column key
    					if (srcPrimaryKey.isComposed()) {
    						throw new MappingConfigurationException("Giving reverse column whereas the primary key is composed :"
    								+ " primary key = [" + srcPrimaryKey.getColumns().stream().map(Column::getName).collect(Collectors.joining(", ")) + "] vs "
    								+ " reverse column = " + relation.getReverseColumnName());
    					}
    					Column<LEFTTABLE, ?> pk = first(srcPrimaryKey.getColumns());
    					reverseColumn = targetTable.addColumn(
    							relation.getReverseColumnName(),
    							pk.getJavaType(),
    							pk.getSize());
    				} else {
    					reverseColumn = (Column<RIGHTTABLE, ?>) relation.getReverseColumn();
    				}
    				foreignKeyBuilder.addColumn(reverseColumn);
    			} else if (relation.giveReverseSetter() != null) {
    				// Please note that here Setter is only used to get foreign key column names, not to set values on reverse side
    				Map<Column<LEFTTABLE, ?>, Column<RIGHTTABLE, ?>> foreignKeyColumnMapping = new HashMap<>();
    				PrimaryKey<LEFTTABLE, SRCID> primaryKey = leftPrimaryKey;
    				AccessorDefinition reverseGetterDefinition = AccessorDefinition.giveDefinition(relation.giveReverseSetter());
    				primaryKey.getColumns().forEach(pkColumn -> {
    					String colName = joinColumnNamingStrategy.giveName(reverseGetterDefinition, pkColumn);
    					Column<RIGHTTABLE, ?> fkColumn = targetTable.addColumn(colName, pkColumn.getJavaType(), pkColumn.getSize(), null);
    					foreignKeyBuilder.addColumn(fkColumn);
    					foreignKeyColumnMapping.put(pkColumn, fkColumn);
    				});
    			} // else : no reverse side mapped, this case can't happen since OneToManyWithMappedAssociationConfigurer is only
    			  // invoked when reverse side is mapped (see OneToManyRelation.isOwnedByReverseSide())
    			Key<RIGHTTABLE, SRCID> foreignKey;
    			foreignKey = foreignKeyBuilder.build();
    			if (relation.isReverseAsMandatory() != null && relation.isReverseAsMandatory()) {
    				foreignKey.getColumns().stream().map(Column.class::cast).forEach(Column::notNull);
    			}
    			return targetTable.addForeignKey(foreignKeyNamingStrategy.giveName(foreignKey, leftPrimaryKey), foreignKey, leftPrimaryKey);
    		}

    	}

    	private static class OneToManyWithAssociationTableHelper<
    			SRC, TRGT, SRCID, TRGTID,
    			LEFTTABLE extends Table<LEFTTABLE>,
    			RIGHTTABLE extends Table<RIGHTTABLE>,
    			ASSOCIATIONTABLE extends AssociationTable<ASSOCIATIONTABLE, LEFTTABLE, RIGHTTABLE, SRCID, TRGTID>> {

    		private final AccessorDefinition accessorDefinitionForTableNaming;

    		public OneToManyWithAssociationTableHelper(AccessorDefinition accessorDefinitionForTableNaming) {
    			this.accessorDefinitionForTableNaming = accessorDefinitionForTableNaming;
    		}

    		protected RelationJoin determineJoin(OneToManyRelation<SRC, TRGT, TRGTID, ?> relation,
    		                                     PrimaryKey<LEFTTABLE, SRCID> leftPrimaryKey,
    		                                     RIGHTTABLE targetTable,
    		                                     AssociationTableNamingStrategy associationTableNamingStrategy,
    		                                     ForeignKeyNamingStrategy foreignKeyNamingStrategy,
    		                                     ColumnNamingStrategy indexColumnNamingStrategy
    		) {
    			// we don't create foreign key for table-per-class because source columns should reference different tables (the one
    			// per entity) which databases do not allow
    			boolean createOneSideForeignKey = !relation.isSourceTablePerClassPolymorphic();
    			boolean createManySideForeignKey = !relation.isTargetTablePerClassPolymorphic();
    			PrimaryKey<RIGHTTABLE, TRGTID> rightPrimaryKey = targetTable.getPrimaryKey();
    			ReferencedColumnNames<LEFTTABLE, RIGHTTABLE> columnNames = associationTableNamingStrategy.giveColumnNames(
    					accessorDefinitionForTableNaming,
    					leftPrimaryKey,
    					rightPrimaryKey);
    			if (relation.getSourceJoinColumnName() != null) {
    				columnNames.setLeftColumnName(Iterables.first(leftPrimaryKey.getColumns()), relation.getSourceJoinColumnName());
    			}
    			if (relation.getSourceJoinColumnName() != null) {
    				columnNames.setRightColumnName(Iterables.first(rightPrimaryKey.getColumns()), relation.getTargetJoinColumnName());
    			}
    			String associationTableName = nullable(relation.getAssociationTableName()).getOr(() -> associationTableNamingStrategy.giveName(accessorDefinitionForTableNaming,
    					leftPrimaryKey, rightPrimaryKey));

    			ASSOCIATIONTABLE intermediaryTable;
    			if (relation.isOrdered()) {
    				String indexingColumnName = nullable(relation.getIndexingColumnName()).getOr(() -> indexColumnNamingStrategy.giveName(accessorDefinitionForTableNaming));

    				intermediaryTable = (ASSOCIATIONTABLE) new IndexedAssociationTable<>(
    						leftPrimaryKey.getTable().getSchema(),
    						associationTableName,
    						leftPrimaryKey,
    						rightPrimaryKey,
    						columnNames,
    						foreignKeyNamingStrategy,
    						createOneSideForeignKey,
    						createManySideForeignKey,
    						indexingColumnName
    				);
    			} else {

    				intermediaryTable = (ASSOCIATIONTABLE) new AssociationTable<>(
    						leftPrimaryKey.getTable().getSchema(),
    						associationTableName,
    						leftPrimaryKey,
    						rightPrimaryKey,
    						columnNames,
    						foreignKeyNamingStrategy,
    						createOneSideForeignKey,
    						createManySideForeignKey
    				);
    			}

    			return new IntermediaryRelationJoin<>(intermediaryTable);
    		}
    	}

    }